#include "catch.hpp"

#include "common/json_schema.h"
#include "common/file.h"
#include "common/log.h"


TEST_CASE("json_schema tests", "[json_schema]") {
    using namespace std;
    using namespace evm;

    JsonSchema schema("schema.json");
    bool valid = schema.validate(Json::parse(File::read_all("schema.json")));
    REQUIRE(valid);

    SECTION("Hello, World!") {
        schema = Json::object();
        REQUIRE(schema.validate(42));
        REQUIRE(schema.validate("I'm a string"));
        REQUIRE(schema.validate({{"an",   {"arbitrarily", "nested"}},
                                 {"data", "structure"}}));
    }

    SECTION("Type-specific keywords") {

        SECTION("string") {
            schema = {{"type", "string"}};
            REQUIRE(schema.validate("I'm a string"));
            REQUIRE(schema.validate(""));
            REQUIRE(!schema.validate(42));

            SECTION("Length") {
                schema = {{"type",      "string"},
                          {"minLength", 2},
                          {"maxLength", 3}};
                REQUIRE(!schema.validate("A"));
                REQUIRE(schema.validate("AB"));
                REQUIRE(schema.validate("ABC"));
                REQUIRE(!schema.validate("ABCD"));
            }

            SECTION("Regular Expressions") {
                schema = {{"type", "string"},
                          {"pattern", R"re(^(\([0-9]{3}\))?[0-9]{3}-[0-9]{4}$)re"}};
                REQUIRE(schema.validate("555-1212"));
                REQUIRE(schema.validate("(888)555-1212"));
                REQUIRE(!schema.validate("(888)555-1212 ext. 532"));
                REQUIRE(!schema.validate("(800)FLOWERS"));
            }
        }

        SECTION("Numeric types") {
            SECTION("integer") {
                schema = {{"type", "integer"}};
                REQUIRE(schema.validate(42));
                REQUIRE(schema.validate(-1));
                REQUIRE(!schema.validate(3.1415926));
                REQUIRE(!schema.validate("42"));

                SECTION("use the mathematical value to determine whether "
                        "a number is an integer, and not the type alone") {
                    schema = {{"type",       "number"},
                              {"multipleOf", 1.0}};
                    REQUIRE(schema.validate(42));
                    REQUIRE(schema.validate(42.0));
                    REQUIRE(!schema.validate(3.1415926));
                }
            }

            SECTION("number") {
                schema = {{"type", "number"}};
                REQUIRE(schema.validate(42));
                REQUIRE(schema.validate(-1));
                REQUIRE(schema.validate(5.0));
                REQUIRE(schema.validate(2.99792458e8));
                REQUIRE(!schema.validate("42"));
            }

            SECTION("Multiples") {
                schema = {{"type",       "number"},
                          {"multipleOf", 10}};
                REQUIRE(schema.validate(0));
                REQUIRE(schema.validate(10));
                REQUIRE(schema.validate(20));
                REQUIRE(!schema.validate(23));
            }

            SECTION("Range") {
                schema = {{"type",    "number"},
                          {"minimum", 0},
                          {"maximum", 100},
                          {"exclusiveMaximum", true}
                };
                REQUIRE(!schema.validate(-1));
                REQUIRE(schema.validate(0));
                REQUIRE(schema.validate(10));
                REQUIRE(schema.validate(99));
                REQUIRE(!schema.validate(100));
                REQUIRE(!schema.validate(101));
            }

        }

        SECTION("object") {
            schema = {{"type", "object"}};
            REQUIRE(schema.validate({{"key",         "value"},
                                     {"another_key", "another_value"}}));
            REQUIRE(!schema.validate("Not an object"));
            REQUIRE(!schema.validate({"An", "array", "not", "an", "object"}));

            SECTION("Properties") {
                Json json({{"type",       "object"},
                           {"properties", {
                                          {"number", {{"type", "number"}}},
                                          {"street_name", {{"type", "string"}}},
                                          {"street_type", {
                                                          {"type", "string"},
                                                          {"enum", {"Street", "Avenue", "Boulevard"}}
                                                          }
                                          }
                                          }
                           }
                          });
                schema = json;
                REQUIRE(schema.validate({{"number",      1600},
                                         {"street_name", "Pennsylvania"},
                                         {"street_type", "Avenue"}}));
                REQUIRE(!schema.validate({{"number",      "1600"},
                                          {"street_name", "Pennsylvania"},
                                          {"street_type", "Avenue"}}));
                REQUIRE(schema.validate({{"number",      1600},
                                         {"street_name", "Pennsylvania"}}));
                REQUIRE(schema.validate(Json({})));
                REQUIRE(schema.validate({{"number",      1600},
                                         {"street_name", "Pennsylvania"},
                                         {"street_type", "Avenue"},
                                         {"direction",   "NW"}}));

                SECTION("additionalProperties false") {
                    json["additionalProperties"] = false;
                    schema = json;
                    REQUIRE(schema.validate({{"number",      1600},
                                             {"street_name", "Pennsylvania"}}));
                    REQUIRE(!schema.validate({{"number",      1600},
                                              {"street_name", "Pennsylvania"},
                                              {"street_type", "Avenue"},
                                              {"direction",   "NW"}}));
                }

                SECTION("additionalProperties object") {
                    json["additionalProperties"] = {{"type", "string"}};
                    schema = json;
                    REQUIRE(schema.validate({{"number",      1600},
                                             {"street_name", "Pennsylvania"}}));
                    REQUIRE(schema.validate({{"number",      1600},
                                             {"street_name", "Pennsylvania"},
                                             {"street_type", "Avenue"},
                                             {"direction",   "NW"}}));
                    REQUIRE(!schema.validate({{"number",        1600},
                                             {"street_name",   "Pennsylvania"},
                                             {"street_type",   "Avenue"},
                                             {"office_number", 201}}));
                }
            }

            SECTION("Properties") {
                Json json({{"type",       "object"},
                           {"properties", {
                                          {"name", {{"type", "string"}}},
                                                   {"email", {{"type", "string"}}},
                                          {"address", {{"type", "string"}}},
                                          {"telephone", {{"type", "string"}}}
                                          }
                           },
                           {"required",   {"name", "email"}}
                          });
                schema = json;
                REQUIRE(schema.validate({{"name",  "William Shakespeare"},
                                         {"email", "bill@stratford-upon-avon.co.uk"}}));
                REQUIRE(schema.validate({
                                        {"name",       "William Shakespeare"},
                                        {"email",      "bill@stratford-upon-avon.co.uk"},
                                        {"address",    "Henley Street, Stratford-upon-Avon, Warwickshire, England"},
                                        {"authorship", "in question"}
                                        }));
                REQUIRE(!schema.validate({
                                         {"name",    "William Shakespeare"},
                                         {"address", "Henley Street, Stratford-upon-Avon, Warwickshire, England"}
                                         }));
            }

            SECTION("Size") {
                schema = {{"type",          "object"},
                          {"minProperties", 2},
                          {"maxProperties", 3}
                };
                REQUIRE(!schema.validate(Json({})));
                REQUIRE(!schema.validate({{"a", 0}}));
                REQUIRE(schema.validate({{"a", 0},
                                         {"b", 1}}));
                REQUIRE(schema.validate({{"a", 0},
                                         {"b", 1},
                                         {"c", 2}}));
                REQUIRE(!schema.validate({{"a", 0},
                                          {"b", 1},
                                          {"c", 2},
                                          {"d", 3}}));
            }

            SECTION("Dependencies") {

                SECTION("Property dependencies") {
                    Json json = {{"type",         "object"},
                                 {"properties",   {
                                                  {"name",        {{"type", "string"}}},
                                                  {"credit_card", {{"type", "number"}}},
                                                  {"billing_address", {{"type", "string"}}},
                                                  }
                                 },
                                 {"required",     {"name"}},
                                 {"dependencies", {
                                                   {"credit_card", {"billing_address"}}
                                                  }
                                 }
                    };
                    schema = json;

                    REQUIRE(schema.validate({{"name",            "John Doe"},
                                             {"credit_card",     5555555555555555},
                                             {"billing_address", "555 Debtor's Lane"}
                                            }));

                    REQUIRE(!schema.validate({{"name",        "John Doe"},
                                              {"credit_card", 5555555555555555}
                                             }));

                    REQUIRE(schema.validate({{"name", "John Doe"}}));

                    REQUIRE(schema.validate({{"name",            "John Doe"},
                                             {"billing_address", "555 Debtor's Lane"}
                                            }));

                    json["dependencies"]["billing_address"] = {"credit_card"};
                    schema = json;

                    REQUIRE(!schema.validate({{"name",        "John Doe"},
                                              {"credit_card", 5555555555555555}
                                             }));

                    REQUIRE(!schema.validate({{"name",            "John Doe"},
                                              {"billing_address", "555 Debtor's Lane"}
                                             }));
                }

                SECTION("Schema dependencies") {
                    Json json = {{"type",         "object"},
                                 {"properties",   {
                                                  {"name",        {{"type",       "string"}}},
                                                  {"credit_card", {{"type", "number"}}}
                                                  }
                                 },
                                 {"required",     {"name"}},
                                 {"dependencies", {
                                                   {"credit_card", {
                                                                    {"properties", {
                                                                                   {"billing_address", {{"type", "string"}}}
                                                                                   }
                                                                    },
                                                                   {"required", {"billing_address"}}
                                                                   }
                                                   }
                                                  }
                                 }
                    };
                    schema = json;

                    REQUIRE(schema.validate({{"name",            "John Doe"},
                                             {"credit_card",     5555555555555555},
                                             {"billing_address", "555 Debtor's Lane"}
                                            }));

                    REQUIRE(!schema.validate({{"name",        "John Doe"},
                                              {"credit_card", 5555555555555555}
                                             }));

                    REQUIRE(schema.validate({{"name",            "John Doe"},
                                             {"billing_address", "555 Debtor's Lane"}
                                            }));
                }
            }

            SECTION("Pattern Properties") {
                Json json = {{"type", "object"},
                             {"patternProperties", {
                                                   {"^S_", {{"type", "string"}}},
                                                   {"^I_", {{"type", "integer"}}}
                                                   }
                             },
                             {"additionalProperties", false}
                };
                schema = json;

                REQUIRE(schema.validate({{"S_25", "This is a string"}}));
                REQUIRE(schema.validate({{"I_0", 42}}));
                REQUIRE(!schema.validate({{"S_0", 42}}));
                REQUIRE(!schema.validate({{"I_42", "This is a string"}}));
                REQUIRE(!schema.validate({{"keyword", "value"}}));

                json["properties"] = {{"builtin", {{"type", "number"}}}};
                json["additionalProperties"] = {{"type", "string"}};
                schema = json;

                REQUIRE(schema.validate({{"builtin", 42}}));
                REQUIRE(schema.validate({{"keyword", "value"}}));
                REQUIRE(!schema.validate({{"keyword", 42}}));
            }
        }

        SECTION("array") {
            schema = {{"type", "array"}};
            REQUIRE(schema.validate({1, 2, 3, 4, 5}));
            REQUIRE(schema.validate({1, "different", {{"type", "of values"}}}));
            REQUIRE(!schema.validate({{"Not", "an array"}}));

            SECTION("Items") {

                SECTION("List validation") {
                    schema = {{"type",  "array"},
                              {"items", {
                                        {"type", "number"}
                                        }
                              }
                    };

                    REQUIRE(schema.validate({1, 2, 3, 4, 5}));
                    REQUIRE(!schema.validate({1, 2, "3", 4, 5}));
                    REQUIRE(schema.validate(Json::array()));
                }

                SECTION("Tuple validation") {
                    Json json = {{"type",  "array"},
                                 {"items", {
                                           {{"type", "number"}},
                                           {{"type", "string"}},
                                           {{"type", "string"}, {"enum", {"Street", "Avenue", "Boulevard"}}},
                                           {{"type", "string"}, {"enum", {"NW", "NE", "SW", "SE"}}}
                                           }
                                 }
                    };
                    schema = json;

                    REQUIRE(schema.validate({1600, "Pennsylvania", "Avenue", "NW"}));
                    REQUIRE(!schema.validate({24, "Sussex", "Drive"}));
                    REQUIRE(!schema.validate({"Palais de l'?lysée"}));
                    REQUIRE(schema.validate({10, "Downing", "Street"}));
                    REQUIRE(schema.validate({1600, "Pennsylvania", "Avenue", "NW", "Washington"}));

                    SECTION("additionalItems") {
                        json["additionalItems"] = false;
                        schema = json;

                        REQUIRE(schema.validate({1600, "Pennsylvania", "Avenue", "NW"}));
                        REQUIRE(schema.validate({1600, "Pennsylvania", "Avenue"}));
                        REQUIRE(!schema.validate({1600, "Pennsylvania", "Avenue", "NW", "Washington"}));
                    }
                }
            }

            SECTION("Length") {
                schema = {{"type",     "array"},
                          {"minItems", 2},
                          {"maxItems", 3}};
                REQUIRE(!schema.validate(Json::array()));
                REQUIRE(!schema.validate({1}));
                REQUIRE(schema.validate({1, 2}));
                REQUIRE(schema.validate({1, 2, 3}));
                REQUIRE(!schema.validate({1, 2, 3, 4}));
            }

            SECTION("Uniqueness") {
                schema = {{"type", "array"},
                          {"uniqueItems", true}};
                REQUIRE(schema.validate({1, 2, 3, 4, 5}));
                REQUIRE(!schema.validate({1, 2, 3, 3, 4}));
                REQUIRE(schema.validate(Json::array()));
            }
        }

        SECTION("array") {
            schema = {{"type", "boolean"}};
            REQUIRE(schema.validate(true));
            REQUIRE(schema.validate(false));
            REQUIRE(!schema.validate("true"));
            REQUIRE(!schema.validate(0));
        }

        SECTION("array") {
            schema = {{"type", "null"}};
            REQUIRE(schema.validate(nullptr));
            REQUIRE(!schema.validate(false));
            REQUIRE(!schema.validate(0));
            REQUIRE(!schema.validate(""));
        }
    }

    SECTION("Enumerated values") {
        schema = {{"type", "string"},
                  {"enum", {"red", "amber", "green"}}};

        REQUIRE(schema.validate("red"));
        REQUIRE(!schema.validate("blue"));

        schema = {{"enum", {"red", "amber", "green", nullptr, 42}}};

        REQUIRE(schema.validate("red"));
        REQUIRE(schema.validate(nullptr));
        REQUIRE(schema.validate(42));
        REQUIRE(!schema.validate(0));

        schema = {{"type", "string"},
                  {"enum", {"red", "amber", "green", nullptr}}};

        REQUIRE(schema.validate("red"));
        REQUIRE(!schema.validate(nullptr));
    }

    SECTION("Combining schemas") {
        schema = {{"anyOf", {
                            {{"type", "string"}, {"maxLength", 5}},
                            {{"type", "number"}, {"minimum", 0}}
                            }
                  }
        };

        REQUIRE(schema.validate("short"));
        REQUIRE(!schema.validate("too long"));
        REQUIRE(schema.validate(12));
        REQUIRE(!schema.validate(-5));

        schema = {{"allOf", {
                            {{"type", "string"}},
                            {{"maxLength", 5}}
                            }
                  }
        };

        REQUIRE(schema.validate("short"));
        REQUIRE(!schema.validate("too long"));

        schema = {{"allOf", {
                            {{"type", "string"}},
                            {{"type", "number"}}
                            }
                  }
        };

        REQUIRE(!schema.validate("No way"));
        REQUIRE(!schema.validate(-1));

        Json json = {{"definitions", {
                                     {"address", {
                                                 {"type", "object"},
                                                 {"properties", {
                                                                {"street_address", {{"type", "string"}}},
                                                                {"city", {{"type", "string"}}},
                                                                {"state", {{"type", "string"}}}
                                                                }
                                                 },
                                                 {"required", {"street_address", "city", "state"}}
                                                 }
                                     }
                                     }
                     },
                     {"allOf",       {
                                     {{"$ref", "#/definitions/address"}},
                                     {{"properties", {
                                                     {"type", {{"enum", {"residential", "business"}}}}
                                                     }
                                      }}
                                     }
                     }
        };

        schema = json;

        REQUIRE(schema.validate({
                                {"street_address", "1600 Pennsylvania Avenue NW"},
                                {"city",           "Washington"},
                                {"state",          "DC"},
                                {"type",           "business"}
                                }));

        /* subschema inside the allOf array do not merge with the root schema,
         * so that the implementation is simple enough, and avoid ambiguousness.
         */
        /*json["additionalProperties"] = false;
        schema = json;

        REQUIRE(!schema.validate({
                                 {"street_address", "1600 Pennsylvania Avenue NW"},
                                 {"city",           "Washington"},
                                 {"state",          "DC"},
                                 {"type",           "business"}
                                 }));*/

        schema = {{"anyOf", {
                            {{"type", "string"}},
                            {{"type", "number"}}
                            }
                  }
        };

        REQUIRE(schema.validate("Yes"));
        REQUIRE(schema.validate(42));
        REQUIRE(!schema.validate({{"Not a", "string or number"}}));

        schema = {{"oneOf", {
                            {{"type", "number"}, {"multipleOf", 5}},
                            {{"type", "number"}, {"multipleOf", 3}}
                            }
                  }
        };

        REQUIRE(schema.validate(10));
        REQUIRE(schema.validate(9));
        REQUIRE(!schema.validate(2));
        REQUIRE(!schema.validate(15));

        schema = {{"not", {{"type", "string"}}}};

        REQUIRE(schema.validate(42));
        REQUIRE(schema.validate({{"key", "value"}}));
        REQUIRE(!schema.validate("I am a string"));
    }

}
